package jrain.fw.script.core;

import java.io.Reader;
import java.io.StringReader;
import java.text.MessageFormat;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

import jrain.fw.script.context.ScriptContext;
import jrain.fw.script.expression.Expression;
import jrain.fw.script.expression.ExpressionString;
import jrain.fw.script.operator.Coercions;
import jrain.fw.script.operator.Constants;
import jrain.fw.script.parser.ELParser;
import jrain.fw.script.parser.ParseException;
import jrain.fw.script.parser.Token;
import jrain.fw.script.parser.TokenMgrError;
import jrain.fw.script.resolver.VariableResolver;
import jrain.fw.script.tools.ELException;
import jrain.fw.script.tools.Logger;

public class ELEvaluator {

	private static final int MAX_SIZE = 100;

	static Map<Object, Object> sCachedExpressionStrings = null;

	static Map<Object, Object> sCachedExpectedTypes = new HashMap<Object, Object>();

	static Logger sLogger = new Logger(System.out);

	VariableResolver mResolver;

	boolean mBypassCache;

	ScriptContext pageContext;

	// -------------------------------------

	public ELEvaluator(VariableResolver pResolver) {
		mResolver = pResolver;
	}

	// -------------------------------------

	public void setBypassCache(boolean pBypassCache) {
		mBypassCache = pBypassCache;
	}

	public Object evaluate(String pExpressionString, Object pContext, Class<?> pExpectedType, Map<Object, Object> functions, String defaultPrefix)
			throws ELException {
		return evaluate(pExpressionString, pContext, pExpectedType, functions, defaultPrefix, sLogger);
	}

	Object evaluate(String pExpressionString, Object pContext, Class<?> pExpectedType, Map<Object, Object> functions, String defaultPrefix, Logger pLogger)
			throws ELException {
		// Check for null expression strings
		if (pExpressionString == null) {
			throw new ELException(Constants.NULL_EXPRESSION_STRING);
		}

		// Set the PageContext;
		pageContext = (ScriptContext) pContext;

		// Get the parsed version of the expression string
		Object parsedValue = parseExpressionString(pExpressionString);

		// Evaluate differently based on the parsed type
		if (parsedValue instanceof String) {
			// Convert the String, and cache the conversion
			String strValue = (String) parsedValue;
			return convertStaticValueToExpectedType(strValue, pExpectedType, pLogger);
		} else if (parsedValue instanceof Expression) {
			// Evaluate the expression and convert
			Object value = ((Expression) parsedValue).evaluate(pContext, mResolver, functions, defaultPrefix, pLogger);
			return convertToExpectedType(value, pExpectedType, pLogger);
		} else if (parsedValue instanceof ExpressionString) {
			// Evaluate the expression/string list and convert
			String strValue = ((ExpressionString) parsedValue).evaluate(pContext, mResolver, functions, defaultPrefix, pLogger);
			return convertToExpectedType(strValue, pExpectedType, pLogger);
		} else {
			// This should never be reached
			return null;
		}
	}

	public Object parseExpressionString(String pExpressionString) throws ELException {
		// See if it's an empty String
		if (pExpressionString.length() == 0) {
			return "";
		}

		if (!(mBypassCache) && (sCachedExpressionStrings == null)) {
			createExpressionStringMap();
		}

		// See if it's in the cache
		Object ret = mBypassCache ? null : sCachedExpressionStrings.get(pExpressionString);

		if (ret == null) {
			// Parse the expression
			Reader r = new StringReader(pExpressionString);
			ELParser parser = new ELParser(r);
			try {
				ret = parser.ExpressionString();
				if (!mBypassCache) {
					sCachedExpressionStrings.put(pExpressionString, ret);
				}
			} catch (ParseException exc) {
				throw new ELException(formatParseException(pExpressionString, exc));
			} catch (TokenMgrError exc) {
				// Note - this should never be reached, since the parser is
				// constructed to tokenize any input (illegal inputs get
				// parsed to <BADLY_ESCAPED_STRING_LITERAL> or
				// <ILLEGAL_CHARACTER>
				throw new ELException(exc.getMessage());
			}
		}
		return ret;
	}

	Object convertToExpectedType(Object pValue, Class<?> pExpectedType, Logger pLogger) throws ELException {
		return Coercions.coerce(pValue, pExpectedType, pLogger);
	}

	Object convertStaticValueToExpectedType(String pValue, Class<?> pExpectedType, Logger pLogger) throws ELException {
		// See if the value is already of the expected type
		if (pExpectedType == String.class || pExpectedType == Object.class) {
			return pValue;
		}

		// Find the cached value
		Map<Object, Object> valueByString = getOrCreateExpectedTypeMap(pExpectedType);
		if (!mBypassCache && valueByString.containsKey(pValue)) {
			return valueByString.get(pValue);
		} else {
			// Convert from a String
			Object ret = Coercions.coerce(pValue, pExpectedType, pLogger);
			valueByString.put(pValue, ret);
			return ret;
		}
	}

	@SuppressWarnings({ "rawtypes", "unchecked" })
	static Map<Object, Object> getOrCreateExpectedTypeMap(Class pExpectedType) {
		synchronized (sCachedExpectedTypes) {
			Map<Object, Object> ret = (Map<Object, Object>) sCachedExpectedTypes.get(pExpectedType);
			if (ret == null) {
				ret = Collections.synchronizedMap(new HashMap());
				sCachedExpectedTypes.put(pExpectedType, ret);
			}
			return ret;
		}
	}

	@SuppressWarnings({ "rawtypes", "serial" })
	private synchronized void createExpressionStringMap() {
		if (sCachedExpressionStrings != null) {
			return;
		}

		final int maxSize = MAX_SIZE;
		// if ((pageContext != null) && (pageContext.getServletContext() != null)) {
		// String value = pageContext.getServletContext().getInitParameter(EXPR_CACHE_PARAM);
		// if (value != null) {
		// maxSize = Integer.valueOf(value);
		// } else {
		// maxSize = MAX_SIZE;
		// }
		// } else {
		// maxSize = MAX_SIZE;
		// }

		// fall through if it couldn't find the parameter
		sCachedExpressionStrings = Collections.synchronizedMap(new LinkedHashMap<Object, Object>() {
			protected boolean removeEldestEntry(Map.Entry eldest) {
				return size() > maxSize;
			}
		});
	}

	// -------------------------------------
	// Formatting ParseException
	// -------------------------------------

	/**
	 * Formats a ParseException into an error message suitable for displaying on a web page
	 */
	static String formatParseException(String pExpressionString, ParseException pExc) {
		// Generate the String of expected tokens
		StringBuffer expectedBuf = new StringBuffer();
		int maxSize = 0;
		boolean printedOne = false;

		if (pExc.expectedTokenSequences == null) {
			return pExc.toString();
		}

		for (int i = 0; i < pExc.expectedTokenSequences.length; i++) {
			if (maxSize < pExc.expectedTokenSequences[i].length) {
				maxSize = pExc.expectedTokenSequences[i].length;
			}
			for (int j = 0; j < pExc.expectedTokenSequences[i].length; j++) {
				if (printedOne) {
					expectedBuf.append(", ");
				}
				expectedBuf.append(pExc.tokenImage[pExc.expectedTokenSequences[i][j]]);
				printedOne = true;
			}
		}
		String expected = expectedBuf.toString();

		// Generate the String of encountered tokens
		StringBuffer encounteredBuf = new StringBuffer();
		Token tok = pExc.currentToken.next;
		for (int i = 0; i < maxSize; i++) {
			if (i != 0) {
				encounteredBuf.append(" ");
			}
			if (tok.kind == 0) {
				encounteredBuf.append(pExc.tokenImage[0]);
				break;
			}
			encounteredBuf.append(addEscapes(tok.image));
			tok = tok.next;
		}
		String encountered = encounteredBuf.toString();

		// Format the error message
		return MessageFormat.format(Constants.PARSE_EXCEPTION, new Object[] { expected, encountered, });
	}

	/**
	 * Used to convert raw characters to their escaped version when these raw version cannot be used as part of an ASCII string literal.
	 */
	static String addEscapes(String str) {
		StringBuffer retval = new StringBuffer();
		char ch;
		for (int i = 0; i < str.length(); i++) {
			switch (str.charAt(i)) {
			case 0:
				continue;
			case '\b':
				retval.append("\\b");
				continue;
			case '\t':
				retval.append("\\t");
				continue;
			case '\n':
				retval.append("\\n");
				continue;
			case '\f':
				retval.append("\\f");
				continue;
			case '\r':
				retval.append("\\r");
				continue;
			default:
				if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) {
					String s = "0000" + Integer.toString(ch, 16);
					retval.append("\\u" + s.substring(s.length() - 4, s.length()));
				} else {
					retval.append(ch);
				}
				continue;
			}
		}
		return retval.toString();
	}

	public String parseAndRender(String pExpressionString) throws ELException {
		Object val = parseExpressionString(pExpressionString);
		if (val instanceof String) {
			return (String) val;
		} else if (val instanceof Expression) {
			return "${" + ((Expression) val).getExpressionString() + "}";
		} else if (val instanceof ExpressionString) {
			return ((ExpressionString) val).getExpressionString();
		} else {
			return "";
		}
	}

}
